/**
 * 
 */
package org.geoscrape.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A simple web client that handles cookies etc.
 * 
 */
public class WebClient
{
	private HashMap<String, String> cookies;
	private String redirectAddress = null;
	private ByteArrayOutputStream contents;
	private String userAgent = "Foozilla";
	private String requestMethod;
	private String lastUrl;
	private String referer;
	private Integer responseCode;
	private int timeout;

	/**
	 * @return the responseCode
	 */
	public Integer getResponseCode()
	{
		return responseCode;
	}

	public WebClient()
	{
		cookies = new HashMap<String, String>();
		contents = new ByteArrayOutputStream();
	}

	protected HttpURLConnection prepareConnection(String urlString) throws IOException
	{
		// make sure the connection string is sane
		if (urlString.startsWith("/"))
		{
			// relative path
			URL oldUrl = new URL(lastUrl);
			urlString = oldUrl.getProtocol() + "://" + oldUrl.getHost() + urlString;
		}
		// reset state
		redirectAddress = null;
		contents.reset();
		// create url
		URL url = new URL(urlString);
		HttpURLConnection con = (HttpURLConnection) url.openConnection();
		con.setUseCaches(false);
		con.setInstanceFollowRedirects(false);
		con.setReadTimeout(timeout);
		con.setConnectTimeout(timeout);
		// set the user agent
		con.setRequestProperty("User-Agent", userAgent);
		if (this.requestMethod != null)
		{
			con.setRequestMethod(this.requestMethod);
		}
		if (this.referer != null)
		{
			con.setRequestProperty("Referer", referer);
		}
		else if (this.lastUrl != null)
		{
			con.setRequestProperty("Referer", this.lastUrl);
		}
		this.lastUrl = urlString;
		// set cookies
		setCookies(con);
		return con;
	}

	public void setReferer(String referer)
	{
		this.referer = referer;
	}

	protected void processResponse(HttpURLConnection con) throws IOException
	{
		// check for errors
		try
		{
			this.responseCode = con.getResponseCode();
		}
		catch (ConnectException e)
		{
			// connection refused
			return;
		}
		// connect to url, get the headers
		Map<String, List<String>> hf = con.getHeaderFields();
		// check headers for cookies
		List<String> entry = hf.get("Set-Cookie");
		// check if this is a set-cookie
		if (entry != null)
		{
			for (String e : entry)
			{
				addCookie(e);
			}
		}

		if (this.responseCode == HttpURLConnection.HTTP_MOVED_PERM
				|| this.responseCode == HttpURLConnection.HTTP_MOVED_TEMP)
		{
			List<String> locations = hf.get("Location");
			if (locations != null && !locations.isEmpty())
			{
				redirectAddress = locations.get(0);
			}
		}
		// read contents
		try
		{
			InputStream is = null;
			if(this.responseCode>=400 && this.responseCode<600)
			{
				is =con.getErrorStream();

			}
			else
			{
				is = con.getInputStream();
			}
			byte[] buffer = new byte[1024];
			int read = is.read(buffer);
			while (read > 0)
			{
				contents.write(buffer, 0, read);
				read = is.read(buffer);
			}
			contents.flush();
		}
		catch (IOException e)
		{
			// ignore, response code handles errors
		}
		finally
		{
			con.disconnect();
		}
	}

	/**
	 * Get a substring of the contents. The substring starts at the end of the
	 * 'from' string and ends at the beginning of the 'to' string.
	 * 
	 * Example: Contents = "The quick brown fox".
	 * 
	 * getContents("quick","fox") returns " brown ";
	 * 
	 * @param from
	 *            marks the beginning of the returned text.
	 * @param to
	 *            marks the end of the returned test.
	 * @return the substring, null if from or to are not found.
	 */
	public String getContentsFromTo(String from, String to)
	{
		return getContentsFromTo(from, to, getContentsAsString());
	}

	protected String getContentsFromTo(String from, String to, String conts)
	{
		int startIndex = conts.indexOf(from);
		if (startIndex >= 0)
		{
			startIndex += from.length();
			int endIndex = conts.indexOf(to, startIndex);
			if (endIndex >= 0)
			{
				return conts.substring(startIndex, endIndex);
			}
		}
		return null;
	}

	/**
	 * Go to a specific url.
	 * 
	 * @param urlString
	 *            the web address to go to.
	 * @throws IOException
	 */
	public void goTo(String urlString) throws IOException
	{
		HttpURLConnection con = prepareConnection(urlString);
		this.responseCode = null;
		processResponse(con);
		con.disconnect();
	}

	/**
	 * Submit a form with the associated values.
	 * 
	 * @param urlString
	 * @param inputs
	 *            a map of key/value pairs.
	 * @throws IOException
	 */
	public void submitForm(String urlString, HashMap<String, String> inputs) throws IOException
	{
		if (!this.getRequestMethod().equalsIgnoreCase("POST"))
		{
			throw new RuntimeException("PUT, GET form submission not yet implemented.");
		}
		HttpURLConnection con = prepareConnection(urlString);
		this.responseCode = null;
		con.setDoOutput(true);
		OutputStreamWriter os = new OutputStreamWriter(con.getOutputStream());
		// write the data to the stream in urlencoded form
		Set<String> keys = inputs.keySet();
		int index = 0;
		for (String key : keys)
		{
			index++;
			String value = inputs.get(key);
			if (value != null)
			{
				os.write(key);
				os.write("=");
				os.write(URLEncoder.encode(value.toString(), "UTF-8"));
				if (index < inputs.size())
				{
					os.write("&");
				}
			}
		}
		os.flush();
		os.close();
		processResponse(con);
	}
	

	/**
	 * Submit a form with the associated values and upload files
	 * 
	 * @param urlString
	 * @param inputs
	 *            a map of key/value pairs.
	 * @param files
	 *            an optional map of key/value pairs, where the key is in the
	 *            form fieldname@filename
	 * @throws IOException
	 */
	public void submitForm(String urlString, Map<String, String> inputs, Map<String, byte[]> files)
			throws IOException
	{
		if (!this.getRequestMethod().equalsIgnoreCase("POST"))
		{
			throw new RuntimeException("PUT, GET form submission not yet implemented.");
		}
		
		String charset ="UTF-8";
		String boundary = "-------"+Long.toHexString(System.currentTimeMillis()); // Just generate some unique random value.
		String CRLF = "\r\n"; // Line separator required by multipart/form-data.
		
		HttpURLConnection con = prepareConnection(urlString);
		con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
		this.responseCode = null;
		con.setDoOutput(true);
		con.setDoInput(true);
		OutputStream output = con.getOutputStream();
		OutputStreamWriter os = new OutputStreamWriter(output,charset);
		PrintWriter writer = new PrintWriter(os, true); // true = autoFlush, important!

		// write the data to the stream 
		Set<String> keys = inputs.keySet();
		for (String key : keys)
		{
			String value = inputs.get(key);
			if (value != null)
			{

			    writer.append( "--"+boundary).append(CRLF);
			    writer.append("Content-Disposition: form-data; name=\""+key+"\"").append(CRLF);
			    writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF);
			    writer.append(CRLF);
			    writer.append(value).append(CRLF).flush();
			}
		}
		// write the files
		if(files!=null)
		{
			keys = files.keySet();
			for(String key: keys)
			{
				byte[] value = files.get(key);
				if(value!=null)
				{
					String [] parts = key.split("@");
					String paramname = parts[0];
					String filename = parts[1];

					String contentType = URLConnection.guessContentTypeFromName(filename);
					
				    // Send binary file.
				    writer.append( "--"+boundary).append(CRLF);
				    writer.append("Content-Disposition: form-data; name=\""+paramname+"\"; filename=\"" + filename+ "\"").append(CRLF);
				    writer.append("Content-Type: " + contentType).append(CRLF);
				    writer.append("Content-Transfer-Encoding: binary").append(CRLF);
				    writer.append(CRLF).flush();
				    output.write(value);
				    output.flush();
				    writer.append(CRLF).flush(); // CRLF is important! It indicates end of binary boundary.

				}
			}
		}
		writer.append("--"+boundary+"--").append(CRLF);
		writer.close();
		
		processResponse(con);
	}

	public String getRequestMethod()
	{
		return requestMethod;
	}

	public void setRequestMethod(String requestMethod)
	{
		this.requestMethod = requestMethod;
	}


	private void setCookies(HttpURLConnection con)
	{
		if (cookies.size() > 0)
		{
			StringBuilder cookieString = new StringBuilder();
			Set<String> keys = cookies.keySet();
			int index = 0;
			for (String k : keys)
			{
				cookieString.append(k);
				cookieString.append("=");
				cookieString.append(cookies.get(k));
				index++;
				if (index < cookies.size())
				{
					cookieString.append("; ");
				}
			}
			con.addRequestProperty("Cookie", cookieString.toString());
		}
	}

	private void addCookie(String e)
	{
		int endIndex = e.indexOf("=");
		if (endIndex > 0)
		{
			String name = e.substring(0, endIndex);
			// check for semicolon
			String value = e.substring(name.length() + 1);
			int semiIndex = value.indexOf(";");
			if (semiIndex > 0)
			{
				value = value.substring(0, semiIndex);
			}
			this.cookies.put(name, value);
		}
	}
	public void addCookie(String name, String value)
	{
		cookies.put(name, value);
	}

	/**
	 * Get the cookies in the client
	 * 
	 * @return
	 */
	public HashMap<String, String> getCookies()
	{
		return cookies;
	}

	public String getRedirectAddress()
	{
		return this.redirectAddress;
	}

	public byte[] getContents()
	{
		return contents.toByteArray();
	}

	public String getContentsAsString()
	{
		String res = null;
		try
		{
			res = new String(getContents(), "UTF8");
		}
		catch (UnsupportedEncodingException e)
		{
			e.printStackTrace();
			res= contents.toString();
		}
		return res;
	}

	public int getContentLenght()
	{
		return this.contents.size();
	}

	public void clearCookies()
	{
		cookies.clear();
	}

	public String getUserAgent()
	{
		return userAgent;
	}

	public void setUserAgent(String userAgent)
	{
		this.userAgent = userAgent;
	}

	public String getLastUrl()
	{
		return lastUrl;
	}

	/**
	 * Set the read timeout in milliseconds
	 * 
	 * @param ms
	 */
	public void setTimeout(int ms)
	{
		this.timeout = ms;
	}

}
